/*
 * Copyright 2008 Neil Bacon
 * Distributed under the terms of the Mozilla Public License version 1.1
 * or the GNU General Public License version 3.
 */

package nb.xadd.node.util;

import static java.lang.Double.isNaN;

import java.io.BufferedWriter;
import java.io.Closeable;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.BitSet;
import java.util.HashMap;
import java.util.Map;

import nb.util.Bounds;
import nb.xadd.XADDRuntimeException;
import nb.xadd.node.Node;
import nb.xadd.node.impl.AbstractDecisionNode;
import nb.xadd.node.impl.ConstantValueNode;
import nb.xadd.node.impl.RealEqualDecisionNode;
import nb.xadd.node.impl.RealGreaterOrEqualDecisionNode;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Exports data for visualization.
 */
public class ExportVisualizationData implements Closeable {

    private Log log = LogFactory.getLog(getClass());

    private final Map<Integer, Bounds> boundsMap = new HashMap<Integer, Bounds>();
    private final BitSet realGreaterOrEqualVars = new BitSet();
    private final Writer writer;

    /**
     * Create a visualization data exporter.
     * @param outputFile data written here (or to stdout if null)
     * @throws IOException
     */
    public ExportVisualizationData(File outputFile) throws IOException {
        writer = new BufferedWriter(outputFile != null ? new FileWriter(outputFile) : new OutputStreamWriter(System.out));
    }

    public void export(String message, Node node) throws IOException {
        boundsMap.clear();
        
        BitSet booleanVars = new BitSet();
        BitSet realEqualVars = new BitSet();
        NodeUtils.getVariables(node, booleanVars, realEqualVars, realGreaterOrEqualVars);
        
        writer.append("# ").append(message).append(": booleanVars = ").append(booleanVars.toString())
            .append(", realEqualVars = ").append(realEqualVars.toString())
            .append(", realGreaterOrEqualVars = ").append(realGreaterOrEqualVars.toString())
            .append('\n');
        walk(node);
        writer.append('\n');
    }
    
    @Override
    public void close() throws IOException {
        writer.close();
    }

    private void walk(Node node) throws IOException {
        if (node instanceof RealGreaterOrEqualDecisionNode) {
            RealGreaterOrEqualDecisionNode dn = (RealGreaterOrEqualDecisionNode) node;
            Bounds bounds = Bounds.getBounds(boundsMap, dn.getRealVarIndex());
            if (log.isDebugEnabled()) {
                log.debug("walk: dn x" + dn.getRealVarIndex() + " >= " + dn.getTestValue() + ", Bounds = " + bounds);
            }

            double min = bounds.min;
            bounds.min = dn.getTestValue(); // descendants get bounds modified according to this test
            walk(dn.getTrueChild());
            bounds.min = min;               // restore current bounds after processing descendants

            double max = bounds.max;
            bounds.max = dn.getTestValue();
            walk(dn.getFalseChild());
            bounds.max = max;
        } else if (node instanceof RealEqualDecisionNode) {
            RealEqualDecisionNode dn = (RealEqualDecisionNode) node;
            Bounds bounds = Bounds.getBounds(boundsMap, dn.getRealVarIndex());
            if (log.isDebugEnabled()) {
                log.debug("walk: dn x" + dn.getRealVarIndex() + " == " + dn.getTestValue() + ", Bounds = " + bounds);
            }

            double min = bounds.min;
            double max = bounds.max;
            bounds.min = bounds.max = dn.getTestValue(); // true descendants get both bounds modified according to this test
            walk(dn.getTrueChild());
            bounds.min = min;               // restore current bounds after processing true descendants
            bounds.max = max;

            walk(dn.getFalseChild());       // false descendants have unmodified bounds
        } else if (node instanceof AbstractDecisionNode) {
            AbstractDecisionNode dn = (AbstractDecisionNode) node;
            walk(dn.getTrueChild());
            walk(dn.getFalseChild());
        } else if (node instanceof ConstantValueNode) {
            ConstantValueNode cn = (ConstantValueNode) node;
            StringBuilder buf = new StringBuilder();
            buf.append(Double.toString(cn.getConstValue()));
            boolean allVarsSpecified = true;
            for (int i = realGreaterOrEqualVars.nextSetBit(0); i >= 0 && allVarsSpecified; i = realGreaterOrEqualVars.nextSetBit(i + 1)) {
                Bounds bounds = Bounds.getBounds(boundsMap, i);
                double x = isNaN(bounds.min)
                    ? (isNaN(bounds.max) ? Double.NaN : bounds.max)
                    : (isNaN(bounds.max) ? bounds.min : (bounds.min + bounds.max) / 2.0);
                if (isNaN(x)) {
                    allVarsSpecified = false;
                    if (log.isDebugEnabled()) {
                        log.debug("x" + i + " not specified for node " + cn);
                    }
                } else {
                    buf.append('\t').append(Double.toString(x));
                }
//                // unbounded value actually applies to all values, but I'll use zero just to get something coming out
//                double x = !isNaN(bounds.min) ? bounds.min : !isNaN(bounds.max) ? bounds.max : 0.0;
//                buf.append('\t').append(Double.toString(x));
            }
            if (allVarsSpecified) {
                writer.append(buf.toString()).append('\n');
            }
        } else {
            throw new XADDRuntimeException("ExportVisualizationData can't handle Node type " + node.getClass().getName());
        }
    }

}
